package br.ufrj.dcc.tp.servidor.model;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;

import br.ufrj.dcc.tp.servidor.interfaces.IClient;
import br.ufrj.dcc.tp.servidor.interfaces.INotifier;
import br.ufrj.dcc.tp.servidor.log.Log;
import br.ufrj.dcc.tp.servidor.log.LogLevel;
import br.ufrj.dcc.tp.utils.Config;
import br.ufrj.dcc.tp.utils.SocketAction;
import br.ufrj.dcc.tp.utils.SocketMessageImpl;
import br.ufrj.dcc.tp.utils.interfaces.ISocketMessage;

public class DefaultClientImpl extends Thread implements IClient {
	
	private String nome;
	private ObjectInputStream in;
	private ObjectOutputStream out;
	private INotifier notifier;
	private Socket socket;
	// Indica que o cliente enviou a mensagem de close.
	private boolean forced;
	
	public DefaultClientImpl( String nome, Socket socket, INotifier notifier ) {
		this.socket = socket;
		this.nome = nome;
		this.notifier = notifier;
		forced = false;
	}
	
	public DefaultClientImpl( String nome, Socket socket, ObjectOutputStream out,
			ObjectInputStream in, INotifier notifier ) {
		
		this(nome, socket, notifier);
		this.in = in;
		this.out = out;
	}
	
	private void processAction(ISocketMessage inMessage) {
		switch(inMessage.getAction()) {
			case LIST_USERS:
				try {
					send(
						new SocketMessageImpl(Server.SERVER_NAME, SocketAction.LIST_USERS, notifier.getSubscriptions())
					);
				} catch( IOException e ) {
					Log.write(this.getClass(), LogLevel.SEVERE, "Erro ao enviar resposta ao cliente.");
					Log.write(this.getClass(), e);
				}
				break;
			case SEND_MESSAGE:
				Log.write(this.getClass(), LogLevel.DEBUG, "Cliente " + nome + " enviou a mensagem: " + inMessage.getBody());
				notifier.update(inMessage);
				break;
			case CLOSE:
				forced = true;
				interrupt();
			break;
		}
	}
	
	@Override
	public void run() {
		ISocketMessage message = null;
		try {
			
			// Liberar o timeout para nao dar exception nesse ponto.
			socket.setSoTimeout(Config.IO_READ_INFINITY.toInteger());
			
			if( in == null ) {
				in = new ObjectInputStream(socket.getInputStream());
			} else if( out == null ) {
				out = new ObjectOutputStream(socket.getOutputStream());
			}
			socket.setSoTimeout(Config.IO_READ_TIMEOUT.toInteger());
			
			while(! isInterrupted()) {
				try {
					message =  null;
					Object obj = null;
					
					synchronized(socket) {
						if( ! socket.isInputShutdown() && ! socket.isClosed() ) {
							obj = in.readObject();
						}
					}
					
					if( obj instanceof ISocketMessage ) {
						message = (ISocketMessage) obj;
					}
					
					if( message != null ) {
						Log.write(this.getClass(), LogLevel.DEBUG, "Cliente " + nome + " requisitou a acao: " + message.getAction());
						processAction(message);
					}
				} catch( InterruptedIOException e ) {
				} catch (IOException e) {
					Log.write(this.getClass(), LogLevel.WARNING, "Mensagem do cliente " + nome + " nao pode ser lida.");
					throw e;
				} catch (ClassNotFoundException e) {
					Log.write(this.getClass(), LogLevel.WARNING, "Objeto enviado por " + nome + " nao e compativel.");
					throw e;
				}
				
				try {
					sleep(Config.SLEEP_TIMEOUT.toInteger());
				} catch (InterruptedException e) {
					Log.write(this.getClass(), LogLevel.WARNING, "Thread do cliente " + nome + " interrompida.");
					break;
				}
			}
			
		} catch( Exception e ) {
			Log.write(this.getClass(), e);
		} finally {
			try {
				notifier.unsubscribe(this, true);
				if( !forced ) {
					disconnect();
				} else {
					forceDisconnect();
				}
			} catch( Exception e ) {}
		}
	}

	@Override
	public String getNome() {
		return nome;
	}

	@Override
	public void send(ISocketMessage message) throws IOException {
		synchronized (socket) {
			if( socket != null && ! socket.isOutputShutdown() && ! socket.isClosed() ) {
				out.writeObject(message);
				out.flush();
			}
		}
	}
	
	// Nesse caso, o cliente enviou um close. Nao eh necessário enviar
	// nada para o cliente de volta.
	private void forceDisconnect() throws IOException {
		if( socket != null && ! socket.isClosed() ) {
			in.close();
			out.close();
			socket.close();
		}
	}
	
	@Override
	public void disconnect() throws IOException {
		interrupt();
		
		send(
			new SocketMessageImpl(Server.SERVER_NAME, SocketAction.CLOSE)
		);
		
		if( socket != null && ! socket.isClosed() ) {
			in.close();
			out.close();
			socket.close();
		}
	}
}
